using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.MultiTenancy;
using Xms.Abp.MultiTenancy;
using Xms.Abp.Threading;

namespace Xms.Abp.BackgroundJob;

/// <summary>
/// Base class that can be used to implement <see cref="IBackgroundSchedule"/>.
/// </summary>
public abstract class BackgroundSchedule : IBackgroundSchedule
{
    //TODO: Add UOW, Localization and other useful properties..?

    public IAbpLazyServiceProvider LazyServiceProvider { get; set; }

    public IServiceProvider ServiceProvider { get; set; }

    protected ILoggerFactory LoggerFactory
        => LazyServiceProvider.LazyGetRequiredService<ILoggerFactory>();

    protected ILogger Logger
        => LazyServiceProvider.LazyGetService<ILogger>(provider => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance);

    protected CancellationTokenSource StoppingTokenSource { get; private set; }
    protected CancellationToken StoppingToken { get; private set; }
    protected IServiceScopeFactory ServiceScopeFactory { get; }
    internal protected AsyncCronTimer Timer { get; }
    public string CronExpression
    {
        get => Timer.CronExpression;
        set => Timer.CronExpression = value;
    }

    public virtual MultiTenancySides MultiTenancySides { get; set; } = MultiTenancySides.Tenant;

    protected IBackgroundJobManager BackgroundJobManager
        => LazyServiceProvider.LazyGetRequiredService<IBackgroundJobManager>();

    internal Func<IBackgroundSchedule, IServiceProvider, DateTime?, CancellationToken, Task> OnExecuted { get; set; }

    public BackgroundScheduleInfo BackgroundScheduleInfo { get; set; }
    internal bool IsRunning { get; set; }

    public BackgroundSchedule(
        AsyncCronTimer timer,
        IServiceScopeFactory serviceScopeFactory)
    {
        ServiceScopeFactory = serviceScopeFactory;
        Timer = timer;
        Timer.Elapsed = TimerElapsed;
        Timer.OnExitRunning = () =>
        {
            IsRunning = false;
            Logger.LogDebug("Stopped background schedule: " + ToString());
            StoppingTokenSource?.Cancel();
            StoppingTokenSource?.Dispose();
            StoppingTokenSource = null;
        };
    }

    public virtual async Task StartAsync(CancellationToken cancellationToken = default)
    {
        if (IsRunning) return;
        IsRunning = true;
        var nextTime = Timer.GetNextOccurrence();
        if (!nextTime.HasValue)
        {
            IsRunning = false;
            Logger.LogWarning("Fail started background schedule: " + ToString());
            return;
        }
        StoppingTokenSource ??= new CancellationTokenSource();
        StoppingToken = StoppingTokenSource.Token;
        await Task.CompletedTask;
        Timer.Start();
        if (!Timer.IsRunning)
        {
            IsRunning = false;
            Logger.LogWarning("Fail started background schedule: " + ToString());
        }
        else
        {
            Logger.LogDebug("Started background schedule: " + ToString() + ",next execute time:" + nextTime.Value.ToString("yyyy-MM-dd HH:mm:ss"));
        }
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken = default)
    {
        if (!IsRunning) return;
        IsRunning = false;
        Timer.Stop();
        Logger.LogDebug("Stopped background schedule: " + ToString());
        StoppingTokenSource?.Cancel();
        StoppingTokenSource?.Dispose();
        StoppingTokenSource = null;
        await Task.CompletedTask;
    }

    public override string ToString()
    {
        return GetType().FullName;
    }

    protected virtual async Task<IEnumerable<Guid?>> GetTenantIdsAsync()
    {
        if (MultiTenancySides == MultiTenancySides.Host)
        {
            return new Guid?[] { null };
        }
        var store = ServiceProvider.GetRequiredService<IMultiTenantStore>();
        var list = (await store.GetAllAsync()).Select(r => (Guid?)r.Id).ToList();
        if (MultiTenancySides == MultiTenancySides.Both)
        {
            list.Insert(0, null);
        }
        return await Task.FromResult(list);
    }

    protected virtual Dictionary<string, object> GetJobArgs(Guid? tenantId)
    {
        return new Dictionary<string, object>
        {
            { "TenantId", tenantId },
        };
    }

    private async Task TimerElapsed(AsyncCronTimer timer)
    {
        Logger.LogDebug("Executing background schedule: " + ToString() + ",next execute time:" + Timer.GetNextOccurrence()?.ToString("yyyy-MM-dd HH:mm:ss"));
        using (var scope = ServiceScopeFactory.CreateScope())
        {
            var serviceProvider = scope.ServiceProvider;
            try
            {
                var tenantIds = await GetTenantIdsAsync();
                foreach (var tenantId in tenantIds)
                {
                    await CreateJobAsync(tenantId, StoppingToken);
                }
                if (OnExecuted != null)
                {
                    await OnExecuted(this, serviceProvider, Timer.GetNextOccurrence(), StoppingToken);
                }
            }
            catch (Exception ex)
            {
                await serviceProvider
                    .GetRequiredService<IExceptionNotifier>()
                    .NotifyAsync(new ExceptionNotificationContext(ex));

                Logger.LogException(ex);
            }
        }
    }

    protected abstract Task CreateJobAsync(Guid? tenantId, CancellationToken cancellationToken = default);

    protected virtual async Task<Guid> EnqueueJobAsync<TArgs>(
        TArgs args,
        Guid? tenantId,
        BackgroundJobPriority priority = BackgroundJobPriority.Normal,
        TimeSpan? delay = null,
        int? timeout = null,
        short? maxTryCount = null,
        IEnumerable<int> retryIntervals = null,
        IEnumerable<Guid> referenceJobs = null,
        CancellationToken stoppingToken = default)
    {
        return await BackgroundJobManager.EnqueueAsync(args, tenantId, priority, delay, timeout, maxTryCount, retryIntervals, referenceJobs, BackgroundScheduleInfo?.Id, stoppingToken);
    }

    protected virtual async Task<Guid> EnqueueJobAsync(
        string jobName,
        string displayName,
        object args,
        Guid? tenantId,
        BackgroundJobPriority priority = BackgroundJobPriority.Normal,
        TimeSpan? delay = null,
        int? timeout = null,
        short? maxTryCount = null,
        IEnumerable<int> retryIntervals = null,
        IEnumerable<Guid> referenceJobs = null,
        Guid? scheduleId = null,
        CancellationToken stoppingToken = default)
    {
        return await BackgroundJobManager.EnqueueAsync(jobName, displayName, args, tenantId, priority, delay, timeout, maxTryCount, retryIntervals, referenceJobs, BackgroundScheduleInfo?.Id, stoppingToken);
    }
}
